問題解説: 謎の国

まずはこちらの資料をご覧下さい.

中国ではインターネットの検閲を行うため,また国内のネットワークを世界中の攻撃から守るため,政府が様々な企業の協力の下に強力なファイアーウォールを構築しています.このファイアウォールにより中国国民はインターネット上での安全がある程度保証されるわけですが,同様にインターネット上での自由を制限されることがしばしばあります.このネットワーク環境はなかなか挙動が興味深く,実際に中国に行ってみると上記の資料にあるような現象に直面します.


さて,今回ICTSC8の謎の国で出題した問題ですが,他の問題とは異なりこの国の問題だけ全て同じ作問者によるものです.上記のような環境で起こりうる問題 (3問目は除く) を順番に解決してもらうことをテーマに作問しました.

全体の概要

参加者に与えられたVM (以降client) は必ず特定のゲートウェイを通るようになっており,ここでLinuxマシン (以降routerとする) が動いていました.そしてこのrouter上でnetfilterを用いてパケットをチェックし,特定の通信しか許可しないLKM (Greatfirewall daemon, 以下GFD) が動いていたため,参加者のVMは自由にインターネットが使用できない状態にありました.
なお,GFDのソースコードおよび他に使用したファイルはこちらにあります.
GFDでは次のような処理を行っていました.

  • UDP
    • ポート53番宛のパケットをrouter自身のdnsmasqによって返答 (DNS)
  • TCP
    • ictscをデータ部分に含むパケットをDROP
    • DNSまたはHTTPではないパケットをDROP
  • その他
    • 制限なし

ここでclientへのDNS応答は本来のサーバーから返ってきたように見せるため,パケットの送信元アドレスを偽装する必要があります.そこでGFDは内部にテーブルを持ち,書き換えたパケットの本来の宛先アドレスを保持し,送信元のIPアドレスとポート番号が,(dnsmasqから)返ってきたパケットの送信先IPアドレスとポート番号に一致したらそれに対する応答である,と判断し,返るパケットの送信元アドレスを本来のアドレスに書き換えます.これによって,clientでは 「8.8.8.8 に問い合わせているのに何を聞いても 127.0.0.1 が返ってくる」という状況を作り出すことができます.

第1のトラブル

最初のトラブルは名前解決ができないというものでした.標準のLinuxではDNSの名前解決にudp/53を使用しますが,GFDによりこのパケットは宛先を問わずrouterのdnsmasqによって解決されてしまい,ほぼすべてのドメインが 127.0.0.1 に向いているため,ドメインを用いた通信が見かけ上できないようになっていました.
DNSではUDPの他にTCPも問い合わせに利用して良いとRFC7766で定められています.試しにTCPで名前解決を行うと,

~$ dig +tcp @8.8.8.8 icttoracon.net
...

;; ANSWER SECTION:
icttoracon.net. 3591 IN A 59.106.171.95

...

ちゃんと名前解決可能であることが確認できます.よって,OSがTCP経由で名前解決をするように設定すれば良いということがわかります.
Linuxでは resolv.conf に名前解決関連の設定を書きますが,ここに RES_USEVC オプションを加えることでシステムがTCPで名前解決をするようになります (man参照) .よって,
/etc/resolv.confoptions use-vcを追記するとトラブルが解決できます.

第2のトラブル

次のトラブルはHTTPSのサイトが見られないというものでした.こちらもGFDによりHTTPとDNS以外のパケットはDROPされてしまうことによるものです.また,この問題ではプロキシ用途として検閲の影響を受けないサーバー (以降server) が与えられているため,与えられたVM (client, server) 同士でトンネルを張るのがゴールとなります.
解法はいくつかありますが,ここでは唯一の解答チームであるuecmmaの方法を紹介します.

  • tinyproxyを用意する (@server)
      • sudo apt-get install tinyproxy
      • /etc/tinyproxy.conf に追記
        • Allow 192.168.18.130/25
        • ConnectPort 22
  • proxyを使って外部サーバーにssh接続, SOCKSプロキシを建てる (@client)
    • ssh -o 'ProxyCommand=ncat --proxy 192.168.18.130:8888 %h %p' 192.168.18.130 -D 9979
  • tsocksを用いて外部サーバー経由で通信 (@client)
    • /etc/tsocks.conf を編集
      • server = 127.0.0.1
      • server_type = 4
      • server_port = 9979
    • tsocks firefox &

以上の手順により, clientとserver間でtunnel over ssh over httpが作成され,自由に通信ができるようになります.

その他の解法としては,HTTPの代わりにDNSやICMPを用いるなどの方法が考えられますが,ICMPはデータ部分が小さいため速度面に問題があります.またDNS tunnelの場合はiodineなどを使うと実現できます.

第3のトラブル

最後のトラブルはGFDに攻撃が仕掛けられ,検閲機能がダウンしてしまうというものでした.参加者にはrouterへのアクセス権が与えられ,検閲機能を構成するGFDのバグ修正を行うことがゴールとなります.この問題を見るためには第2のトラブルを解決する必要がありますが,到達したのはuecmmaのみでした.
この問題が開いてしばらくするとUDPでの名前解決ができるようになり,またTCP/UDP全ての通信が通ります.router上でパケットキャプチャをすると 192.168.18.100 から不審なパケットが飛んできており,どうやらこれが原因ということがわかります.パケットの内容は以下の通りです.

  • UDP
    • srcが 192.168.18.253 に偽装されたDNS問い合わせ
  • TCP
    • SYNフラグが立ったパケットがひたすら送られてくる

GFDのソースコードを読むと,これらの攻撃は全て dns_table および tcp_table を溢れさせるためのものということがわかります.そしてこの脆弱性の原因はレスポンスがあるという前提で,書き換えたパケットの情報をテーブルに保持していることにあります.つまり,応答が存在しないようなパケットを送りつけることでどんどんテーブルにエントリが蓄積されていき,dns_table_push および tcp_table_push ができなくなることでパケットの書き換えが行われなくなるのがポイントです.

これを修正するにはレスポンスが存在しないことを想定して,タイムアウト処理を加えます.まずは時間を保持するtimestampをメンバに加えます.

typedef struct {
uint32_t saddr;
uint32_t daddr;
uint16_t sport;
uint64_t timestamp;
} DNS_ENTRY;

typedef struct {
uint32_t saddr;
uint32_t daddr;
uint16_t sport;
uint16_t dport;
uint8_t state;
uint64_t timestamp;
} TCP_ENTRY;

次にpush時にエントリを全て参照するタイミングで,時間のチェックを行います.カーネル空間では do_gettimeofday を用いて時刻を取得できます.また同様にして,pushするエントリにも現在の時刻を付与します.

// dns_table
struct timeval tv;
do_gettimeofday(&tv);
uint64_t cur = tv->tv_sec * 1000000 + tv->tv_usec;

while (start != (dns_idx&(TABLE_SIZE-1))) {
...
  if (table[dns_idx].timestamp + TIMEOUT < cur) { // timeout
    table[dns_idx].sport = 0;
  }
  ...
  if (!table[dns_idx].sport) { // push entry
    ...
    table[dns_idx].timestamp = cur;
  }
}

これにより,push時に古いエントリが削除されるためテーブルが溢れにくくなります.また dns_table および tcp_table はリングバッファになっているため,局所的にエントリが書き込まれるといったことが起こらず,定期的に全てのエントリがチェックされます.

また,このLKMではレースコンディション対策がされていないため,各テーブルを参照するタイミングでロックが必要です.

static DEFINE_MUTEX(dns_table_mutex);
static DEFINE_MUTEX(tcp_table_mutex);

...
  mutex_lock(&dns_table_mutex);
  ...
  mutex_unlock(&dns_table_mutex);
...

この問題においては以上の対策で十分ですが,膨大なリソースを使った攻撃をされた場合処理が追いつかない可能性があるため,テーブルのサイズを大きめに取る,という方法でもある程度対策ができます.

#define TABLE_SIZE (1<<12)

講評

やや特殊な問題設定だったこともあり,第1のトラブルの時点で7チームしか回答を提出しておらず,通過チームについては3チームにとどまりました.最初に述べたような環境の存在を知っていればゴールが見えやすかったと思いますが,実際に経験してみないと対処方法がわからないというトラコンの特徴を体感できたのではないでしょうか.また,第2のトラブルで用いる技術は制限を回避するためだけでなく,信頼できない環境で安全な通信路を確保する手段でもあります.公衆無線LANなどの環境では,いつ誰が個人情報を狙っていてもおかしくない時代です.今回の問題で,途中経路に攻撃者がいる場合は簡単に通信を改竄することが可能であるということがご理解頂けたと思います.
第3のトラブルですが,これはネットワークに加えてLinuxの知識が多少必要になる問題なので,他の問題とは全くコンセプトが異なります.「インフラ系のコンテストでまさかLKMに触れる問題が出るとは思わないだろう」という邪な考えから作成した問題なので,あまり解かれることを想定していませんでした.
この問題を通して検閲ネットワークの雰囲気を少しでも体感して頂けたのであれば幸いです.